/*******************************************************************************
 * Copyright (c) 2009 David Harrison.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl-3.0.html 
 *  
 * Contributors:
 *     David Harrison - initial API and implementation
 ******************************************************************************/
package com.sfs.postcodes.dao;

import java.util.ArrayList;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.sql.DataSource;
 
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.dao.IncorrectResultSizeDataAccessException;


public class PostcodeDAOImpl implements PostcodeDAO {
     
    
    private static final long serialVersionUID = 1L;
    private JdbcTemplate jdbcTemplate = null;        
    private String _newzealandPostcode = "";    
    private String _internationalPostcode = "";    
    final private Logger dataLogger = Logger.getLogger(PostcodeDAO.class);
    
    
    public void setDataSource(final DataSource dataSource)
    {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
       
    /**
     * Takes an address formatted in the following manner
     * street,suburb,city,state,country and checks for a postcode
     * Intended to work for New Zealand and Australian addresses
     * @param address
     * @param state
     * @param country
     * @return
     * @throws com.sfs.postcodes.dao.PostcodeLookupException
     */
    public String findPostcode(final String address, final String state, 
            final String country) throws PostcodeLookupException
    {        
        String postCode = "";
        
        if(address==null) {
            throw new PostcodeLookupException("Sorry the address string cannot "
                    + "be null when performing a postcode lookup");
        }
        if(country==null) {
            throw new PostcodeLookupException("Sorry the country string cannot "
                    + "be null when performing a postcode lookup");
        }
        if(state==null) {
            throw new PostcodeLookupException("Sorry the state string cannot "
                    + "be null when performing a postcode lookup");
        }
        
        // Load the SQL strings
        try {
        	ResourceBundle sqlBundle = ResourceBundle
        		.getBundle("com.sfs.postcodes.dao.sql");
        	
        	this._newzealandPostcode = sqlBundle
        		.getString("postcode.newzealand");
        	this._internationalPostcode  = sqlBundle
        		.getString("postcode.international");
        	
        } catch (Exception e) {
            dataLogger.error("Error loading postcode SQL strings: "
                    + e.getMessage());
        }        
        
        ArrayList<String> addressArray = new ArrayList<String>();
        
        // Replace any instances of a carriage return with a comma
        String addressString = StringUtils.replace(address, "\n", ",");
        
        StringTokenizer st = new StringTokenizer(addressString,",");
        while (st.hasMoreTokens()) {
            String component = st.nextToken().trim();
            addressArray.add(component);
        }
        
        if(country.compareToIgnoreCase("New Zealand")==0) {
            postCode = findNewZealandPostcode(addressArray);
        }
        
        if(country.compareToIgnoreCase("Australia")==0 
                && state.compareTo("")!=0) {
            postCode = findInternationalPostcode(addressArray, state, country);
        }
        return postCode;        
    }
    
    
    private String findNewZealandPostcode(ArrayList<String> addressArray) 
            throws PostcodeLookupException 
    {
        // Perform a postcode search for an Australian address
        String postCode = "";
        
        try {  
            /**
             * Loop through the address array performing a search
             * for a postcode. Begin with a tight address and zoom out 
             * with the intention of getting the most precise postcode
             **/
                
            for(int a = 0; a < addressArray.size(); a++) {

                for(int b = addressArray.size(); b > 0; b--) {

                    // If a postcode has been found do not perform the search
                    if(postCode.compareTo("")==0) {                    

                        StringBuffer sb = new StringBuffer();
                        // Build search string
                        for(int y = 0; y < b; y++) {

                            int key = y + a;
                            if(key < b) {
                                if(sb.length()>0) {
                                    sb.append(",");
                                }
                                String searchComponent = addressArray.get(key)
                                        .trim().toUpperCase();
                                searchComponent = StringUtils.replace(
                                        searchComponent, "P.O. ", "PO ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "P.O.", "PO ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "Post Office ", "PO ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "R.D. ", "R D ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "R.D.", "R D ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "Rural Delivery ", 
                                        "R D ");

                                sb.append(searchComponent);                     
                            }
                        }
                        postCode = performPostCodeLookup(sb.toString());
                    }
                }
            }
        } catch (Exception e) {
            dataLogger.error("Error performing New Zealand postcode search: " 
                    + e.getMessage());
        }        
        return postCode;
    }
    
    
    private String findInternationalPostcode(
            final ArrayList<String> addressArray, final String state,
            final String country) throws PostcodeLookupException
    {
        // Perform a postcode search for an Australian address
        String postCode = "";
        
        try {
                            
            // If a postcode has already been found do not repeat the search
            if(StringUtils.isBlank(postCode)) {
                
                /**
                 * Now loop through the address array performing a 
                 * search for a postcode. Begin broad and zoom in with 
                 * the intention of getting the most precise postcode
                 **/

                for(int i = addressArray.size(); i > 0; i--) {
                    try {
                        postCode = (String) this.jdbcTemplate.queryForObject(
                            _internationalPostcode,
                            new Object[]{
                                addressArray.get(i-1).trim().toUpperCase(), 
                                state.toUpperCase(), 
                                country.toUpperCase()
                            },
                            new RowMapper() {
                                public Object mapRow(ResultSet rs, int rowNum)
                                        throws SQLException {
                                    return rs.getString("postcode");
                                }
                            });                            
                        
                        dataLogger.info("Postcode: " + postCode);
                        
                    } catch (IncorrectResultSizeDataAccessException ie) {
                        // No results found for this search - keep looking
                    }
                }
            }        
        } catch (Exception e) {
            dataLogger.error("Error performing international postcode search: "
                    + e.getMessage());
        }
        return postCode;
    }
    
    
    private String performPostCodeLookup(final String address)
    {
        String postCode = "";
        // If the search string is empty do not perform the search
        if(address!=null && address.length()>0) {
            
            dataLogger.debug("Looking up address: " + address);
            
            postCode = performJdbcSearch(address);
            
            dataLogger.debug("Postcode: " + postCode);
            
            if(postCode.length()==0) {
                
                dataLogger.debug("No address found on first pass");
                
                // No postcode found, check to see if it begins with a number
                
                // Create a pattern to addresses beginning with a number
                Pattern pattern = Pattern.compile("^\\d");
                Matcher match = pattern.matcher(address);
                
                if(match.lookingAt()) {
                    dataLogger.debug("The address begins with a number");
                    
                    // If a match perform a search based on the address string
                    // minus the starting number.
                    try {
                        final String clippedAddress = address.substring(
                                address.indexOf(" ")+1, address.length());

                        dataLogger.debug("Revised address: " + clippedAddress);

                        postCode = performJdbcSearch(clippedAddress);
                        dataLogger.debug("Postcode: " + postCode);
                        
                    } catch (Exception e) {
                        dataLogger.error("Error performing postcode (decimal) "
                                + "search: " + e.getMessage());
                    }
                }
            }            
        }
        return postCode;
    }
    
    
    private String performJdbcSearch(final String address)
    {
        String postCode = "";
        try {
            postCode = (String) this.jdbcTemplate.queryForObject(
                _newzealandPostcode,
                new Object[]{address + "%"},
                new RowMapper() {
                    public Object mapRow(ResultSet rs, int rowNum)
                            throws SQLException {
                        return rs.getString("postcode");
                    }
                });
                
            dataLogger.info("Postcode: " + postCode);

        } catch (IncorrectResultSizeDataAccessException ie) {
            // No results found for this search - keep looking
        }
        return postCode;
    }
    
}

